// vPresent is an immersive presentation creation and display application.
// Copyright (C) 2007-2009 by Priority 5 Holdings, Inc.
//
// This program is free software; you can redistribute it and/or modify it
// under the terms of the GNU General Public License as published by the Free
// Software Foundation; either version 2 of the License, or (at your option)
// any later version.
//
// This program is distributed in the hope that it will be useful, but WITHOUT
// ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
// FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License for
// more details.
//
// You should have received a copy of the GNU General Public License along
// with this program; if not, write to the Free Software Foundation, Inc.,
// 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.

#define X11     1
#define WINDOWS 2
#define COCOA   3

#if defined(WIN32) || defined(WIN64)
#  define WINDOW_SYS WINDOWS
#elif defined(__APPLE__)
#  define WINDOW_SYS COCOA
#else
#  define WINDOW_SYS X11
#endif

#include <sstream>
#include <boost/shared_ptr.hpp>
#include <boost/program_options.hpp>

#if WINDOW_SYS == X11
#include <X11/Xatom.h>
#endif

#include <OpenSG/OSGConfig.h>
#include <OpenSG/OSGClusterServer.h>
#if WINDOW_SYS == X11
#include <OpenSG/OSGXWindow.h>
#elif WINDOW_SYS == COCOA
#include <OpenSG/OSGCocoaWindow.h>
#elif WINDOW_SYS == WINDOWS
#include <OpenSG/OSGWIN32Window.h>
#else
#error "Unknown window system in use"
#endif

#include <OpenSG/OSGRenderAction.h>


namespace po = boost::program_options;

#if WINDOW_SYS == X11
namespace
{

template<int EventType>
int waitForEvent(Display*, XEvent* event, char* arg)
{
   return event->type == EventType && event->xmap.window == (::Window) arg;
}

}
#endif

class WindowHandler
{
public:
   WindowHandler(const bool fullScreen, const bool alwaysOnTop,
                 const bool stereo, const bool exitOnError,
                 const std::string& name, const std::string& connectionType,
                 const std::string& address, const OSG::UInt32 servicePort,
                 const std::string& serviceGroup)
      : mFullScreen(fullScreen)
      , mAlwaysOnTop(alwaysOnTop)
      , mStereo(stereo)
      , mExitOnError(exitOnError)
      , mName(name)
      , mConnectionType(connectionType)
      , mAddress(address)
      , mServicePort(servicePort)
      , mServiceGroup(serviceGroup)
      , mDone(false)
#if WINDOW_SYS == X11
      , mDisplay(NULL)
      , mXWindow(0)
      , mVisualInfo(NULL)
#endif
   {
      mRenderAction = RenderActionPtr(OSG::RenderAction::create());
   }

   ~WindowHandler()
   {
      mWindow = OSG::NullFC;

#if WINDOW_SYS == X11
      if ( NULL != mDisplay && 0 != mXWindow )
      {
         XDestroyWindow(mDisplay, mXWindow);
      }
#endif
   }

   const OSG::WindowRefPtr getWindow() const
   {
      return mWindow;
   }

   const OSG::WindowRefPtr makeWindow(int* argc, char** argv)
   {
#if WINDOW_SYS == X11
      int dbl_buffered[16];

      dbl_buffered[0] = GLX_RGBA;
      dbl_buffered[1] = GLX_DEPTH_SIZE;
      dbl_buffered[2] = 16;
      dbl_buffered[3] = GLX_DOUBLEBUFFER;
      dbl_buffered[4] = mStereo ? GLX_STEREO : None;
      dbl_buffered[5] = None;

      GLboolean double_buffer = GL_FALSE;

      // X init
      mDisplay = XOpenDisplay(NULL);

      if ( NULL == mDisplay )
      {
         std::cerr << "Error: Could not open display!" << std::endl;
      }

      int dummy;

      if ( ! glXQueryExtension(mDisplay, &dummy, &dummy) )
      {
         std::cerr << "Error: X server has no OpenGL GLX extension!"
                   << std::endl;
      }

      mVisualInfo = glXChooseVisual(mDisplay, DefaultScreen(mDisplay),
                                    dbl_buffered);

      if ( NULL == mVisualInfo )
      {
         int single_buffered[] = { GLX_RGBA, GLX_DEPTH_SIZE, 16, None };
         mVisualInfo = glXChooseVisual(mDisplay, DefaultScreen(mDisplay),
                                       single_buffered);

         if ( NULL == mVisualInfo )
         {
            std::cerr << "No RGB visual with depth buffer!" << std::endl;
         }

         double_buffer = GL_FALSE;
      }

      if ( mVisualInfo->c_class != TrueColor )
      {
         std::cerr << "TrueColor visual required for this program!"
                   << std::endl;
      }

      Colormap cmap =
         XCreateColormap(mDisplay, RootWindow(mDisplay, mVisualInfo->screen),
                         mVisualInfo->visual, AllocNone);

      XSetWindowAttributes swa;
      swa.colormap     = cmap;
      swa.border_pixel = 0;
      swa.event_mask   = ExposureMask         |
                         ButtonPressMask      |
                         ButtonReleaseMask    |
                         KeyPressMask         |
                         Button1MotionMask    |
                         Button2MotionMask    |
                         Button3MotionMask    |
                         StructureNotifyMask;

      // Create Window

      // Create a Window and connect it to the main display.
      mXWindow = XCreateWindow(mDisplay,
                               RootWindow(mDisplay, mVisualInfo->screen),
                               0, 0, 300, 300, 0, mVisualInfo->depth,
                               InputOutput, mVisualInfo->visual,
                               CWBorderPixel | CWColormap | CWEventMask,
                               &swa);

      std::ostringstream win_name;
      win_name << "vPresent: " << mName;
      XSetStandardProperties(mDisplay, mXWindow, win_name.str().c_str(),
                             "vPresent", None, argv, *argc, NULL);

      if ( mFullScreen )
      {
         // Set the decoration hints.
         Atom no_decor_atom = XInternAtom(mDisplay, "_MOTIF_WM_HINTS", 0);

         if ( no_decor_atom == None )
         {
            std::cerr << "Could not intern X atom for _MOTIF_WM_HINTS."
                      << std::endl;
         }

         struct NoDecorHints
         {
            long flags;
            long functions;
            long decorations;
            long input_mode;
         };

         NoDecorHints hints;

         hints.flags = 2;
         hints.decorations = 0;

         XChangeProperty(mDisplay, mXWindow, no_decor_atom, no_decor_atom, 32,
                         PropModeReplace, (unsigned char*) &hints, 4);

//         Atom fullscreen_hint = XInternAtom(mDisplay,
//                                            "_NET_WM_STATE_FULLSCREEN", 0);
         // Create an empty cursor.
         static char data[1] = {0};

         Pixmap blank;
         XColor dummyCol;

         blank = XCreateBitmapFromData(mDisplay, mXWindow, data, 1, 1);

         mEmptyCursor = XCreatePixmapCursor(mDisplay, blank, blank, &dummyCol,
                                            &dummyCol, 0, 0);

         XFreePixmap(mDisplay, blank);
      }

      if ( mAlwaysOnTop )
      {
         Atom net_wm_state = XInternAtom(mDisplay, "_NET_WM_STATE", 0);

         Atom above_hint = XInternAtom(mDisplay, "_NET_WM_STATE_ABOVE", 0);
         Atom always_hint = XInternAtom(mDisplay,
                                        "_NET_WM_STATE_STAYS_ON_TOP", 0);

         Atom net_winstates[] = { above_hint, always_hint };
         XChangeProperty(mDisplay, mXWindow, net_wm_state, XA_ATOM, 32,
                         PropModeReplace, (unsigned char*) net_winstates, 2);
      }

      OSG::XWindowRefPtr xwin(OSG::XWindow::create());
      xwin->setDisplay(mDisplay);
      xwin->setWindow(mXWindow);
      xwin->init();
      mWindow = OSG::WindowRefPtr(xwin);
#endif

      return mWindow;
   }

   void activateWindow()
   {
      mWindow->activate();
   }

   void showWindow()
   {
#if WINDOW_SYS == X11
      XEvent event;

      XMapWindow(mDisplay, mXWindow);
      XIfEvent(mDisplay, &event, waitForEvent<MapNotify>, (char*) mXWindow);

      if ( mFullScreen )
      {
         XMoveWindow(mDisplay, mXWindow, 0, 0);
         XResizeWindow(mDisplay, mXWindow,
                       DisplayWidth(mDisplay, mVisualInfo->screen),
                       DisplayHeight(mDisplay, mVisualInfo->screen));

         XDefineCursor(mDisplay, mXWindow, mEmptyCursor);
         XFlush(mDisplay);
      }
#endif
   }

   /**
    * Hides the window but does not destroy it or any resources associated
    * with this object.
    */
   void hideWindow()
   {
#if WINDOW_SYS == X11
      XUnmapWindow(mDisplay, mXWindow);
      XEvent event;
      XIfEvent(mDisplay, &event, waitForEvent<UnmapNotify>, (char*) mXWindow);

      // TODO: Restore the regular mouse cursor?
#endif
   }

   /**
    * Returns only if \c mExitOnError is true and an error occurs during
    * communication with the client (including disconnection).
    */
   void runEventLoop()
   {
#if WINDOW_SYS == X11
      int ip;
      XEvent event;

      mDone = false;

      while ( ! mDone )
      {
         while ( ip = XPending(mDisplay) )
         {
            XNextEvent(mDisplay, &event);

            switch ( event.type )
            {
                case ConfigureNotify:
                  reshape(event.xconfigure.width, event.xconfigure.height);
                  break;

                case Expose:
                   render();
                   break;

             }
          }

         render();
      }
#endif
   }

   void startServer()
   {
      // create the cluster server
      mServer = ClusterServerPtr(new OSG::ClusterServer(mWindow, mName,
                                                        mConnectionType,
                                                        mAddress /*,
                                                        mServicePort,
                                                        mServiceGroup */));

      // start the server
      mServer->start();
   }

private:
   void render()
   {
      try
      {
         mServer->render(mRenderAction.get());
         OSG::Thread::getCurrentChangeList()->clear();
      }
      catch (std::exception& ex)
      {
         if ( mExitOnError )
         {
            std::cerr << ex.what() << std::endl;
            mDone = true;
//            OSG::osgExit();
//            std::exit(EXIT_FAILURE);
         }
         else
         {
            mServer->stop();
            hideWindow();
            mServer->start();
            showWindow();
         }
      }
   }

   void reshape(const int width, const int height)
   {
      mWindow->resize(width, height);
   }

   bool mFullScreen;
   bool mAlwaysOnTop;
   bool mStereo;
   bool mExitOnError;
   std::string mName;
   std::string mConnectionType;
   std::string mAddress;
   OSG::UInt32 mServicePort;
   std::string mServiceGroup;

   typedef boost::shared_ptr<OSG::RenderAction> RenderActionPtr;
   typedef boost::shared_ptr<OSG::ClusterServer> ClusterServerPtr;

   RenderActionPtr   mRenderAction;
   OSG::WindowRefPtr mWindow;
   ClusterServerPtr  mServer;

   bool mDone;

#if WINDOW_SYS == X11
   Display*     mDisplay;
   Window       mXWindow;
   XVisualInfo* mVisualInfo;
   Cursor       mEmptyCursor;
#endif
};

int main(int argc, char** argv)
{
   bool fullscreen(false);
   bool always_on_top(false);
   bool stereo(false);
   bool exit_on_error(false);
   std::string address;
   std::string name("ClusterServer");
   std::string connection_type("StreamSock");
   OSG::UInt32 service_port(8437);
   std::string service_group("224.245.211.234");

   try
   {
      po::options_description prog_opts("Options");

      prog_opts.add_options()
         ("help,h", "Print help message")
         ("exit-on-error,e", "Exit on error")
         ("fullscreen,f", "Run in fullscreen mode")
         ("always-on-top,A", "Open window above all others")
         ("stereo,s", "Enable stereoscopic rendering")
         ("name,n", po::value<std::string>()->default_value(name),
          "Our server name (if machine:port, then acts as address also).")
         ("address,a", po::value<std::string>()->default_value(address),
          "Address on which we wait for an incoming connection.")
         ("type,t", po::value<std::string>()->default_value("StreamSock"),
          "Communication method to use: StreamSock, Multicast, SockPipeline")
         ("port,p", po::value<OSG::UInt32>()->default_value(service_port),
          "Service port to use.")
         ("group,g", po::value<std::string>()->default_value(service_group),
          "Service group to use.")
         ;

      po::variables_map vm;
      po::store(po::command_line_parser(argc, argv).options(prog_opts).run(),
                vm);
      po::notify(vm);

      if ( vm.count("help") > 0 )
      {
         std::cout << prog_opts << std::endl;
         return 0;
      }

      if ( vm.count("name") > 0 )
      {
         name = vm["name"].as<std::string>();
      }

      if ( vm.count("address") > 0 )
      {
         address = vm["address"].as<std::string>();
      }

      if ( vm.count("type") > 0 )
      {
         connection_type = vm["type"].as<std::string>();
         if ( ! (std::string("StreamSock") == connection_type ||
                 std::string("Multicast") == connection_type ||
                 std::string("SockPipeline") == connection_type) )
         {
            std::cerr << "Invalid connection type '" << connection_type << "'"
                      << std::endl;
            connection_type = "StreamSock";
         }
      }

      if ( vm.count("port") > 0 )
      {
         service_port = vm["port"].as<OSG::UInt32>();
      }

      if ( vm.count("group") > 0 )
      {
         service_group = vm["group"].as<std::string>();
      }

      exit_on_error = vm.count("exit-on-error") > 0;
      fullscreen    = vm.count("fullscreen") > 0;
      always_on_top = vm.count("always-on-top") > 0;
      stereo        = vm.count("stereo") > 0;
   }
   catch (std::exception& ex)
   {
      std::cerr << ex.what() << std::endl;
      return EXIT_FAILURE;
   }

   try
   {
      OSG::osgInit(argc, argv);

      WindowHandler wh(fullscreen, always_on_top, stereo, exit_on_error, name,
                       connection_type, address, service_port, service_group);
      wh.makeWindow(&argc, argv);
      wh.activateWindow();

      glEnable(GL_LIGHTING);
      glEnable(GL_LIGHT0);
      glEnable(GL_NORMALIZE);

      wh.startServer();

      wh.showWindow();
      wh.runEventLoop();
   }
   catch (std::exception& ex)
   {
      std::cerr << ex.what() << std::endl;
      OSG::osgExit();
      return EXIT_FAILURE;
   }

   return EXIT_SUCCESS;
}
